import { MigrateBuilder } from './migrate-builder';
import {
  ExecutableStatement,
  SQL,
  KeyColumn,
  XObjectName,
  DbType,
  ColumnDeclareForAlter,
  CreateTableBuilder,
  CreateTableMember,
  ColumnDeclareForAdd,
} from '../core';
import { MigrateScripter } from './migrate-scripter';
import { pack } from './util';
import {
  ColumnSchema,
  ConstraintSchema,
  ForeignKeySchema,
  IndexSchema,
  PrimaryKeySchema,
  SequenceSchema,
  TableSchema,
} from './schema';
import { SchemaComparator } from './schema-comparator';

export class SqlMigrateScripter extends MigrateScripter<ExecutableStatement> {
  constructor(
    comparator: SchemaComparator,
    private readonly builder: MigrateBuilder
  ) {
    super(comparator);
  }

  annotation(note: string): void {
    this.middleCodes.push(SQL.annotation(note));
  }

  createTable(table: TableSchema): void {
    this.middleCodes.push(
      this.builder.createTable(table).as(g => [
        ...table.columns.map(schema =>
          this.tableColumnForAdd(g.column, schema)
        ),
        g.primaryKey(table.primaryKey!.name).on(
          table.primaryKey!.columns.map(
            c =>
              ({
                name: c.name,
                sort: c.isAscending ? 'ASC' : 'DESC',
              } as KeyColumn)
          )
        ),
      ])
    );
  }

  dropTable(table: TableSchema): void {
    this.middleCodes.push(this.builder.dropTable(table));
  }
  // useDatabase(name: string): void {
  //   this.beforeCodes.push(this.builder.use(name));
  // }
  // createDatabase(database: DatabaseSchema): void {
  //   const sql = this.builder.createDatabase(database.name);
  //   if (database.collate) {
  //     sql.collate(database.collate);
  //   }
  //   this.beforeCodes.push(sql);
  // }
  // alterDatabase(database: DatabaseSchema): void {
  //   this.beforeCodes.push(
  //     this.builder.alterDatabase(database.name).collate(database.collate!)
  //   );
  // }
  // dropDatabase(name: string): void {
  //   this.beforeCodes.push(this.builder.dropDatabase(name));
  // }

  insertSeedData(table: TableSchema, data: any[]): void {
    const fields = table.columns
      .filter(col => !col.isCalculate)
      .map(col => col.name);
    const rows = data.map(item => {
      const row: Record<string, any> = {};
      fields.forEach(field => (row[field] = item[field]));
      return row;
    });
    const identityColumn = table.columns.find(col => col.isIdentity);
    const sql = this.builder.insert(table).values(rows);
    if (identityColumn) {
      sql.withIdentity();
    }
    this.seedDataCodes.set(table, sql);
  }

  createIndex(table: XObjectName, index: IndexSchema): void {
    const sql = this.builder.createIndex(index.name).on(
      table,
      index.columns.map(
        col =>
          ({
            name: col.name,
            sort: col.isAscending ? 'ASC' : 'DESC',
          } as KeyColumn)
      )
    );
    if (index.isUnique) {
      sql.unique();
    }
    if (index.isClustered) {
      sql.clustered();
    }
    this.middleCodes.push(sql);
  }

  dropIndex(table: XObjectName, name: string): void {
    this.middleCodes.push(this.builder.dropIndex(table, name));
  }

  addForeignKey(table: XObjectName, fk: ForeignKeySchema) {
    const sql = this.builder.alterTable(table).add($ => {
      const v = $.foreignKey(fk.name)
        .on(fk.columns)
        .reference(
          { name: fk.referenceTable, schema: fk.referenceSchema },
          fk.referenceColumns
        );
      if (fk.isCascade) {
        v.deleteCascade();
      }
      return v;
    });

    this.afterCodes.push(sql);
  }

  dropForeignKey(table: XObjectName, name: string) {
    this.beforeCodes.push(this.builder.alterTable(table).dropForeignKey(name));
  }

  commentColumn(table: XObjectName<string>, name: string, comment?: string) {
    if (comment) {
      this.middleCodes.push(
        ...pack(this.builder.setColumnComment(table, name, comment))
      );
    } else {
      this.middleCodes.push(
        ...pack(this.builder.dropColumnComment(table, name))
      );
    }
  }

  commentTable(table: XObjectName, comment?: string) {
    if (comment) {
      this.middleCodes.push(
        ...pack(this.builder.setTableComment(table, comment))
      );
    } else {
      this.middleCodes.push(...pack(this.builder.dropTableComment(table)));
    }
  }

  commentIndex(table: XObjectName, name: string, comment?: string) {
    if (comment) {
      this.middleCodes.push(
        ...pack(this.builder.setIndexComment(table, name, comment))
      );
    } else {
      this.middleCodes.push(
        ...pack(this.builder.dropIndexComment(table, name))
      );
    }
  }

  commentConstraint(table: XObjectName, name: string, comment?: string) {
    if (comment) {
      this.middleCodes.push(
        ...pack(this.builder.setConstraintComment(table, name, comment))
      );
    } else {
      this.middleCodes.push(
        ...pack(this.builder.dropConstraintComment(table, name))
      );
    }
  }

  commentForeignKey(table: XObjectName, name: string, comment?: string) {
    if (comment) {
      this.middleCodes.push(
        ...pack(this.builder.setConstraintComment(table, name, comment))
      );
    } else {
      this.middleCodes.push(
        ...pack(this.builder.dropConstraintComment(table, name))
      );
    }
  }

  setDefaultValue(table: XObjectName, column: string, defaultValue: string) {
    this.middleCodes.push(
      ...pack(
        this.builder.setDefaultValue(table, column, SQL.raw(defaultValue))
      )
    );
  }

  dropDefaultValue(table: XObjectName, column: string) {
    this.middleCodes.push(
      ...pack(this.builder.dropDefaultValue(table, column))
    );
  }

  setAutoRowflag(table: XObjectName, column: string) {
    this.middleCodes.push(...pack(this.builder.setAutoRowflag(table, column)));
  }

  dropAutoRowflag(table: XObjectName, column: string) {
    this.middleCodes.push(...pack(this.builder.dropAutoRowflag(table, column)));
  }

  dropIdentity(table: XObjectName, column: string) {
    this.middleCodes.push(...pack(this.builder.dropIdentity(table, column)));
  }

  setIdentity(
    table: XObjectName,
    column: string,
    start: number,
    increment: number
  ) {
    this.middleCodes.push(
      ...pack(this.builder.setIdentity(table, column, start, increment))
    );
  }

  createSequence(sequence: SequenceSchema): void {
    this.middleCodes.push(
      this.builder
        .createSequence(sequence)
        .as(SQL.raw(sequence.type))
        .startWith(sequence.startValue ?? 0)
        .incrementBy(sequence.increment ?? 1)
    );
  }

  dropSequence(name: SequenceSchema): void {
    this.middleCodes.push(this.builder.dropSequence(name));
  }

  addColumn(table: XObjectName, schema: ColumnSchema): void {
    this.middleCodes.push(
      this.builder
        .alterTable(table)
        .add(({ column }) => this.tableColumnForAdd(column, schema))
    );
  }

  dropColumn(table: XObjectName, name: string): void {
    this.middleCodes.push(this.builder.alterTable(table).dropColumn(name));
  }

  alterColumn(table: XObjectName, column: ColumnSchema): void {
    this.middleCodes.push(
      this.builder
        .alterTable(table)
        .alterColumn(g => this.tableColumnForAlter(g, column))
    );
  }

  addPrimaryKey(table: XObjectName, pk: PrimaryKeySchema): void {
    this.middleCodes.push(
      this.builder.alterTable(table).add(({ primaryKey }) =>
        primaryKey(pk.name).on(
          pk.columns.map(
            col =>
              ({
                name: col.name,
                sort: col.isAscending ? 'ASC' : 'DESC',
              } as KeyColumn)
          )
        )
      )
    );
  }

  dropPrimaryKey(table: XObjectName, name: string): void {
    this.middleCodes.push(this.builder.alterTable(table).dropPrimaryKey(name));
  }

  addConstraint(table: XObjectName, constraint: ConstraintSchema): void {
    this.middleCodes.push(
      this.builder.alterTable(table).add(g => {
        switch (constraint.kind) {
          case 'CHECK':
            return g.check(constraint.name, SQL.raw(constraint.sql));
          // case 'UNIQUE':
          //   return g.uniqueKey(constraint.name).on(
          //     constraint.columns.map(
          //       col =>
          //         ({
          //           name: col.name,
          //           sort: col.isAscending ? 'ASC' : 'DESC',
          //         } as KeyColumn)
          //     )
          //   );
        }
      })
    );
  }

  dropConstraint(table: XObjectName, constraint: ConstraintSchema): void {
    switch (constraint.kind) {
      case 'CHECK':
        this.middleCodes.push(
          this.builder.alterTable(table).dropCheck(constraint.name)
        );
        break;
      // case 'UNIQUE':
      //   this.middleCodes.push(
      //     this.builder.alterTable(table).dropUniqueKey(constraint.name)
      //   );
      //   break;
    }
  }

  private tableColumnForAlter(
    g: (name: string, type: DbType) => ColumnDeclareForAlter,
    schema: ColumnSchema
  ): ColumnDeclareForAlter {
    const col: ColumnDeclareForAlter = g(schema.name, DbType.raw(schema.type));
    if (schema.isNullable) {
      col.null();
    } else {
      col.notNull();
    }
    return col;
  }

  private tableColumnForAdd(
    column: CreateTableBuilder['column'],
    schema: ColumnSchema
  ): CreateTableMember {
    const col: ColumnDeclareForAdd = column(
      schema.name,
      DbType.raw(schema.type)
    );
    if (schema.isNullable) {
      col.null();
    } else {
      col.notNull();
    }
    if (schema.isIdentity) {
      col.identity(schema.identityStartValue, schema.identityIncrement);
    }
    if (schema.isCalculate) {
      col.as(SQL.raw(schema.calculateExpression!));
    }
    if (schema.isIdentity) {
      col.identity(schema.identityStartValue, schema.identityIncrement);
    }
    if (schema.isCalculate) {
      col.as(SQL.raw(schema.calculateExpression!));
    }
    if (schema.defaultValue !== null && schema.defaultValue !== undefined) {
      col.default(SQL.raw(schema.defaultValue));
    }
    return col;
  }
}
